Sprite Fusion Tile Attributes
Intro
Every 2D gamedev knows the pain: you've got a beautiful tilemap, but now you need to add gameplay elements. Where do enemies spawn? Which tiles are collectibles? What triggers that secret door?
The traditional solution? Maintain separate data structures, coordinate systems, and hope they stay in sync with your map. It's tedious, error-prone, and breaks the moment you resize your level.
In this post, I'll show you how the SpriteFusion tile attributes feature changes the game. With the updated ExcaliburJS SpriteFusion plugin, you can now embed custom JSON data directly into your tilemap — keeping your logic and layout in perfect harmony.
What Are Tile Attributes?
Tile attributes let you attach custom JSON data to any tile in your SpriteFusion map. Think of it as adding metadata that travels with your tiles.
Instead of manually tracking "enemy at position (10, 6)" in your code, you mark that tile in SpriteFusion:
json{"id": "1","x": 10,"y": 6,"attributes": {"entity": "mushroom","health": 50,"drops": ["coin", "powerup"]}}
json{"id": "1","x": 10,"y": 6,"attributes": {"entity": "mushroom","health": 50,"drops": ["coin", "powerup"]}}
When your game loads, the plugin reads this data and hands it to you — ready to spawn entities, configure behaviors, or drive game logic.
Why This Matters
Before Tile Attributes
The old workflow looked like this:
- Design your map in SpriteFusion
- Export as JSON
- Open your code editor
- Manually add entity spawn data with hardcoded coordinates
- Test the game
- Realize you need to move something
- Update both the map AND your spawn coordinates
- Repeat forever
Result: Two sources of truth that constantly drift apart.
With Tile Attributes
The new workflow:
- Design your map in SpriteFusion
- Add attributes directly to tiles where you want entities/logic
- Export as JSON
- Let the plugin handle everything
Result: One source of truth. Change your map, and your game logic updates automatically.
How It Works in SpriteFusion
As of October 2025, SpriteFusion added tile attributes to their editor. Here's how to use them:
- Select a tile in your map
- Open the Tile Attributes panel
- Add your JSON data — any valid JSON object works
- Export as JSON (not "save" — the plugin needs the JSON export)
Your exported JSON now includes an attributes field:
json{"tileSize": 16,"mapWidth": 30,"mapHeight": 12,"layers": [{"name": "ObjectLayer","tiles": [{ "id": "0", "x": 25, "y": 3, "attributes": { "entity": "bottle" } },{ "id": "1", "x": 10, "y": 6, "attributes": { "entity": "mushroom" } },{ "id": "5", "x": 4, "y": 4, "attributes": { "entity": "knight" } }],"collider": false}]}
json{"tileSize": 16,"mapWidth": 30,"mapHeight": 12,"layers": [{"name": "ObjectLayer","tiles": [{ "id": "0", "x": 25, "y": 3, "attributes": { "entity": "bottle" } },{ "id": "1", "x": 10, "y": 6, "attributes": { "entity": "mushroom" } },{ "id": "5", "x": 4, "y": 4, "attributes": { "entity": "knight" } }],"collider": false}]}
The Updated Plugin API
The ExcaliburJS SpriteFusion plugin now supports two powerful features for working with tile attributes:
1. Attribute Callbacks
Pass a callback function to process tile attributes as the map loads:
typescriptconst attributeCallback = (attData: TileAttributeData) => {const { tileData, mapData } = attData;const { attributes, x, y, id } = tileData;// Spawn entities based on attribute dataif (attributes.entity === 'mushroom') {const enemy = new Mushroom({pos: vec(x * mapData.tileSize, y * mapData.tileSize),health: attributes.health || 50});game.add(enemy);}};const spriteFusionMap = new SpriteFusionResource({mapPath: './map/map.json',spritesheetPath: './map/spritesheet.png',tileAttributeFactory: attributeCallback});
typescriptconst attributeCallback = (attData: TileAttributeData) => {const { tileData, mapData } = attData;const { attributes, x, y, id } = tileData;// Spawn entities based on attribute dataif (attributes.entity === 'mushroom') {const enemy = new Mushroom({pos: vec(x * mapData.tileSize, y * mapData.tileSize),health: attributes.health || 50});game.add(enemy);}};const spriteFusionMap = new SpriteFusionResource({mapPath: './map/map.json',spritesheetPath: './map/spritesheet.png',tileAttributeFactory: attributeCallback});
The callback receives:
- tileData: The specific tile's data including
id,x,y, andattributes - mapData: The full map configuration for context
2. Object Layers
Sometimes you want a layer purely for data — no visual tiles, just positions and attributes. That's what object layers are for.
Mark layers as object layers, and the plugin will:
- ✅ Parse all tile attributes and call your callback
- ❌ Skip rendering the layer as a visual tilemap
typescriptconst spriteFusionMap = new SpriteFusionResource({mapPath: './map/map.json',spritesheetPath: './map/spritesheet.png',tileAttributeFactory: attributeCallback,objectLayers: ['ObjectLayer', 'SpawnPoints', 'Triggers']});
typescriptconst spriteFusionMap = new SpriteFusionResource({mapPath: './map/map.json',spritesheetPath: './map/spritesheet.png',tileAttributeFactory: attributeCallback,objectLayers: ['ObjectLayer', 'SpawnPoints', 'Triggers']});
This is perfect for:
- Enemy spawn points
- Item placement
- Trigger zones
- Waypoint paths
- Anything that needs position data without visual tiles
A Complete Example: Enemy Spawner
Let's walk through a practical example. We'll create a tilemap with embedded enemy data and spawn them automatically.
Step 1: Design Your Map
In SpriteFusion:
- Create a layer called "Enemies"
- Place tiles where you want enemies to spawn
- Add attributes to each tile:
json{ "entity": "goblin", "patrol": true, "range": 3 }{ "entity": "slime", "speed": 2 }
json{ "entity": "goblin", "patrol": true, "range": 3 }{ "entity": "slime", "speed": 2 }
Step 2: Set Up Your Entities
typescriptclass Goblin extends ex.Actor {constructor(config: { pos: Vector, patrol?: boolean, range?: number }) {super({pos: config.pos,width: 16,height: 16,color: ex.Color.Green});if (config.patrol) {this.setupPatrol(config.range || 2);}}setupPatrol(range: number) {// Add patrol behavior}}class Slime extends ex.Actor {constructor(config: { pos: Vector, speed?: number }) {super({pos: config.pos,width: 16,height: 16,color: ex.Color.Blue,vel: vec(config.speed || 1, 0)});}}
typescriptclass Goblin extends ex.Actor {constructor(config: { pos: Vector, patrol?: boolean, range?: number }) {super({pos: config.pos,width: 16,height: 16,color: ex.Color.Green});if (config.patrol) {this.setupPatrol(config.range || 2);}}setupPatrol(range: number) {// Add patrol behavior}}class Slime extends ex.Actor {constructor(config: { pos: Vector, speed?: number }) {super({pos: config.pos,width: 16,height: 16,color: ex.Color.Blue,vel: vec(config.speed || 1, 0)});}}
Step 3: Create Your Attribute Callback
typescriptconst spawnEntities = (attData: TileAttributeData) => {const { tileData, mapData } = attData;const { attributes, x, y } = tileData;// Calculate world positionconst worldPos = vec(x * mapData.tileSize + mapData.tileSize / 2,y * mapData.tileSize + mapData.tileSize / 2);// Spawn based on entity typelet entity: ex.Actor | null = null;switch (attributes.entity) {case 'goblin':entity = new Goblin({pos: worldPos,patrol: attributes.patrol,range: attributes.range});break;case 'slime':entity = new Slime({pos: worldPos,speed: attributes.speed});break;}if (entity) {game.add(entity);}};
typescriptconst spawnEntities = (attData: TileAttributeData) => {const { tileData, mapData } = attData;const { attributes, x, y } = tileData;// Calculate world positionconst worldPos = vec(x * mapData.tileSize + mapData.tileSize / 2,y * mapData.tileSize + mapData.tileSize / 2);// Spawn based on entity typelet entity: ex.Actor | null = null;switch (attributes.entity) {case 'goblin':entity = new Goblin({pos: worldPos,patrol: attributes.patrol,range: attributes.range});break;case 'slime':entity = new Slime({pos: worldPos,speed: attributes.speed});break;}if (entity) {game.add(entity);}};
Step 4: Load Your Map
typescriptconst game = new ex.Engine({width: 800,height: 600});const spriteFusionMap = new SpriteFusionResource({mapPath: './map/dungeon.json',spritesheetPath: './map/spritesheet.png',tileAttributeFactory: spawnEntities,objectLayers: ['Enemies'] // Don't render this layer});const loader = new ex.Loader([spriteFusionMap]);game.start(loader).then(() => {spriteFusionMap.addToScene(game.currentScene);// All enemies are now spawned with their custom data!});
typescriptconst game = new ex.Engine({width: 800,height: 600});const spriteFusionMap = new SpriteFusionResource({mapPath: './map/dungeon.json',spritesheetPath: './map/spritesheet.png',tileAttributeFactory: spawnEntities,objectLayers: ['Enemies'] // Don't render this layer});const loader = new ex.Loader([spriteFusionMap]);game.start(loader).then(() => {spriteFusionMap.addToScene(game.currentScene);// All enemies are now spawned with their custom data!});
Advanced Use Cases
Tile attributes aren't just for spawning entities. Here are more ways to use them:
Interactive Tiles
json{"type": "door","locked": true,"key": "brass_key","destination": "level_2"}
json{"type": "door","locked": true,"key": "brass_key","destination": "level_2"}
Environmental Effects
json{"hazard": "lava","damage": 10,"interval": 1000}
json{"hazard": "lava","damage": 10,"interval": 1000}
Quest Markers
json{"npc": "merchant","dialog": "quest_intro","items": ["potion", "map"]}
json{"npc": "merchant","dialog": "quest_intro","items": ["potion", "map"]}
Pathfinding Data
json{"node": true,"connections": [12, 45, 67],"cost": 2}
json{"node": true,"connections": [12, 45, 67],"cost": 2}
Best Practices
Keep Attributes Focused
Don't overload attributes with everything. Use them for:
- ✅ Position-dependent data (spawn points, triggers)
- ✅ Configuration that should live with the map
- ❌ Complex game logic better suited for separate systems
Use Object Layers Wisely
Visual layers and object layers serve different purposes:
- Visual layers: Render the tilemap, optionally include attributes
- Object layers: Pure data, no rendering
If a tile has both visual and data requirements, keep them on separate layers for clarity.
Validate Your Attributes
The plugin passes whatever JSON is in SpriteFusion. Add validation:
typescriptconst attributeCallback = (attData: TileAttributeData) => {const { attributes } = attData.tileData;if (!attributes.entity) {console.warn('Tile missing entity attribute:', attData);return;}// Safe to use attributes.entity now};
typescriptconst attributeCallback = (attData: TileAttributeData) => {const { attributes } = attData.tileData;if (!attributes.entity) {console.warn('Tile missing entity attribute:', attData);return;}// Safe to use attributes.entity now};
Benefits of This Approach
1. Single Source of Truth
Your map IS your spawn data. No synchronization issues.
2. Designer-Friendly
Level designers can place and configure entities without touching code.
3. Iteration Speed
Move an enemy? Just drag the tile. Change stats? Update the attributes. Export and test.
4. Type Safety (with TypeScript)
Define your attribute schemas:
typescriptinterface EnemyAttributes {entity: 'goblin' | 'slime' | 'boss';health?: number;patrol?: boolean;range?: number;}const attributeCallback = (attData: TileAttributeData) => {const attrs = attData.tileData.attributes as EnemyAttributes;// TypeScript knows what's available};
typescriptinterface EnemyAttributes {entity: 'goblin' | 'slime' | 'boss';health?: number;patrol?: boolean;range?: number;}const attributeCallback = (attData: TileAttributeData) => {const attrs = attData.tileData.attributes as EnemyAttributes;// TypeScript knows what's available};
Common Pitfalls
Forgetting to Export as JSON
SpriteFusion has both "Save" and "Export JSON" options. The plugin needs the JSON export, not the saved project file.
Mixing Visual and Data Responsibilities
If your callback is making decisions based on tile graphics, you're coupling too tightly. Use attributes for data, tile IDs for visuals.
Overcomplicating Attributes
Keep them simple. If you're nesting 5 levels deep in your JSON, consider moving that logic elsewhere.
Installation and Setup
Get started in three steps:
1. Install the Plugin
bashnpm install @excaliburjs/plugin-spritefusion
bashnpm install @excaliburjs/plugin-spritefusion
2. Create Your Map in SpriteFusion
- Visit https://www.spritefusion.com/editor
- Design your map
- Add tile attributes where needed
- Export as JSON
3. Load in Excalibur
typescriptimport { SpriteFusionResource } from '@excaliburjs/plugin-spritefusion';const map = new SpriteFusionResource({mapPath: './map/map.json',spritesheetPath: './map/spritesheet.png',tileAttributeFactory: yourCallback,objectLayers: ['DataLayer']});game.start(loader).then(() => {map.addToScene(game.currentScene);});
typescriptimport { SpriteFusionResource } from '@excaliburjs/plugin-spritefusion';const map = new SpriteFusionResource({mapPath: './map/map.json',spritesheetPath: './map/spritesheet.png',tileAttributeFactory: yourCallback,objectLayers: ['DataLayer']});game.start(loader).then(() => {map.addToScene(game.currentScene);});
Why ExcaliburJS
Small plug for the engine that makes this all possible:
ExcaliburJS is a friendly, TypeScript 2D game engine for the web. It's free and open source (FOSS), well documented, and has a growing community of developers building great games.
The SpriteFusion plugin is just one example of how Excalibur's architecture makes complex features feel natural. If you're interested in 2D web game development, check it out!
Join the Discord community for questions and support.
Summary
Tile attributes bridge the gap between level design and game logic. What used to require maintaining parallel data structures now happens automatically:
- Design once — add entities, triggers, and logic directly in SpriteFusion
- One source of truth — no more coordinate synchronization
- Iterate fast — move things in the editor, not the code
The updated ExcaliburJS SpriteFusion plugin makes this seamless with attribute callbacks and object layers. Your maps become more than just visuals — they're living configuration files for your game world.
Whether you're spawning enemies, placing collectibles, or defining trigger zones, tile attributes keep your workflow smooth and your codebase clean.
Resources
Ready to embed game logic in your tilemaps? Give SpriteFusion attributes a try — your future self will thank you when you move that boss fight for the tenth time and everything just works.
